# coding=utf-8

import os.path
import time

import requests
import traceback
import json
import random


def remove_suffix(input_string, suffix):
    if input_string.endswith(suffix):
        return input_string[:-len(suffix)]
    return input_string


def configInstall(default_config: str, needConfigure=False):
    import inspect
    filename = inspect.stack()[1].filename
    configFilename = remove_suffix(filename, ".py") + "_config.py"

    if not os.path.exists(configFilename):
        with open(configFilename, "w") as f:
            firstLineData = default_config.split("\n")[0]
            space = len(firstLineData) - len(firstLineData.lstrip())
            f.write(default_config.replace(firstLineData[:space], ""))
            if needConfigure:
                print(f"核心插件配置初始化成功，请去{configFilename}/配置自动化服务信息")
            # exit(-1)


configInstall(
    """
    import sys

    # nodered短信推送网关
    nodered_gateway = ""
    # nodered 异常、错误 推送网关
    nodered_error_platform = ""
    # data 文件保存路径
    conf_save_dir = sys.path[0]
    # 是否开启debug模式
    DEBUG = False
    """, True
)

from .autoService_config import *

"""基本配置"""
DIR_STORAGE = f"{conf_save_dir}/storage"  # 登录session等信息存储位置
DIR_CONF = f"{conf_save_dir}/conf"  # 自动化批量登录数据保存位置
DIR_OUTPUT = f"{conf_save_dir}/output"  # 结果保存输出位置
DIR_TMP = f"{conf_save_dir}/tmp"  # 临时文件目录
dir_list = [
    DIR_STORAGE, DIR_CONF, DIR_OUTPUT, DIR_TMP
]

for _bdir_ in dir_list:
    if not os.path.exists(_bdir_):
        os.mkdir(_bdir_)

'''面向过程'''
sms_union_id = 0
APPLICATION_NAME = "autoServiceApplication"
__NUM__ = "0"


def check_node_status():
    try:
        data = requests.get(nodered_gateway + "/state").json()
        if 'code' not in data:
            return False
        elif data["code"] == 200:
            return True
        return False
    except Exception as e:
        print(e)
        return False


def setNum(num):
    global __NUM__
    # 设置手机号码
    __NUM__ = str(num)


def checkUserUsePhone(num=__NUM__):
    try:
        p = 0
        data = None
        while p < 3:
            data = requests.get(nodered_gateway + f"/getPhoneState?num={num}").json()
            if data["code"] == 201:
                # 平台报告该手机无需解锁就可以得到通知
                return True
            if "timeSinceChangedMs" not in data:
                p = p + 1
                time.sleep(2)
                continue
            break

        errorCheck(p < 3, f"nodered暂未添加该手机配置文件号码为:{num}.", app_name="autoService")

        during = data["timeSinceChangedMs"]
        if data["state"] == "on":
            log("等待手机亮屏中", model="autoService", level="NOTICE")
        elif DEBUG:
            # 调试模式则不用等待3分钟
            return True
        elif during < 3 * 60 * 1000:

            log("等待手机持续亮屏中，已亮屏%s秒,总进度:%s%%" % (
                int(during / 1000), str(round((during / 1000) / (3 * 60) * 100, 2))),
                model="autoService", level="NOTICE")
        else:
            return True

        return False
    except Exception as e:
        # print(e)
        # print(nodered_gateway + f"/getPhoneState?num={num}")
        traceback.print_exc()
        return False


def init(appName):
    global APPLICATION_NAME
    while not check_node_status():
        log("节点链接失败，60s后重连", model="autoService", level="NOTICE")
        time.sleep(60)
    APPLICATION_NAME = appName

    while not checkUserUsePhone():
        time.sleep(10)


def rsa_sign(rsa_key, data):
    rsa_key = f'-----BEGIN PUBLIC KEY-----\n{rsa_key}\n-----END PUBLIC KEY-----\n'
    msg = data.encode("utf-8")
    import rsa
    import base64
    h = '{"type":"cat","limit":"2"}'
    bp = bytes(rsa_key, encoding='utf8')
    pk = rsa.PublicKey.load_pkcs1_openssl_pem(bp)
    bh = msg  # bytes(msg, encoding='utf8')
    m = rsa.encrypt(bh, pk)
    enc_data = base64.b64encode(m).decode('utf-8')
    return enc_data


def getTimeStr():
    return time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time()))


def errorCheck(b: bool, *msg, app_name=APPLICATION_NAME):
    '''
    错误上报并结束程序
    '''
    import traceback
    if b == False or b == None:
        log(*msg, model=app_name, level="FATAL")

        upload_data = {"msg": f"[{app_name}] {msg[0]}", "note": ""}
        if len(msg) >= 2:
            upload_data.update({"note": " ".join(msg[1:])})
        msg_list = traceback.format_stack()[:-2]
        upload_data["note"] = upload_data["note"] + "\ncall stack:\n" + "".join(msg_list)
        if nodered_error_platform.strip() != "":
            requests.post(
                nodered_error_platform.strip() + "/submitErrorMsg",
                data=json.dumps(upload_data),
                headers={"Content-Type": "application/json"}
            )
        raise ValueError(msg[0])
        # exit(-1)


def log(*args, **kwargs):
    '''
    日志函数
    :param level:str
    :return:
    '''
    kwargs.setdefault("level", "INFO")
    if 'level' in kwargs and kwargs["level"] == "DEBUG":
        if not DEBUG:
            return

    if "model" in kwargs and kwargs["model"] is not None:
        model_name = kwargs["model"]
    else:
        model_name = APPLICATION_NAME
    time = getTimeStr()
    level = kwargs["level"]
    print(f"{time} [{level}] [{model_name}]:", *args)


def sms_send(func, *args, phone=__NUM__, application="", **argslist):
    '''
    验证码发送包装器,让手机错峰获取验证码。
    '''
    # argslist.setdefault("application", "")

    trycount = 0
    st = 0
    try:
        while trycount < 500:
            trycount = trycount + 1

            data = requests.get(
                nodered_gateway + f'/setSmsSendTime?num={phone}&application={application}').json()

            # print("进入发送检测")
            if data["code"] == 0:
                time.sleep(random.randint(1000, 3000) / 1000)
                data = requests.get(
                    nodered_gateway + f'/setSmsSendTime?num={phone}&application={application}').json()
                if data["application"] == application:
                    break
                # log("错误信息",data["application"] ,application,model=application)
            elif data["code"] == 1:
                st = round(data["time"] + random.randint(2, 10), 3)
                log("发送验证码错峰等待", st, "秒", model="autoService", level="NOTICE")
                time.sleep(st)

        #    sms_union_id = data["union"]
        # return True
    except Exception as e:
        # print(e)
        errorCheck(True, "验证码发送包装器错误!", e)
        # return False

    return func(st, *args, **argslist)


def sms_getlast_union():
    '''
    初始化短信API服务
    '''
    global sms_union_id
    try:
        data = requests.get(nodered_gateway + f'/getVerCode?num={__NUM__}').json()
        sms_union_id = data["union"]
        return True
    except Exception as e:
        print(e)
        return False


def getSmsVerCode(phone=__NUM__, call_is_obj=False, union_id=sms_union_id):
    '''
    获取手机端验证码,请务必调用一次sms_getlast_union初始化一次后再调用其他服务发送短信
    :return:
    '''
    global sms_union_id
    i = 0
    while i < 12:
        try:
            data = requests.get(nodered_gateway + f'/getVerCode?num={phone}').json()
            if data["union"] != union_id:
                log(data, model="NetWorkTransfer", level="DEBUG")
                if call_is_obj:
                    return data["data"], data["union"]
                else:
                    sms_union_id = data["union"]
                return data["data"]
            i = i + 1
        except Exception as e:
            print(e)
        time.sleep(5)
    log("短信获取超时，已退出getSmsVerCode方法", model="autoService", level="NOTICE")

    if call_is_obj:
        return "", union_id
    else:
        return ""


"""空运行器"""


def blank_run(*args, **kwargs):
    pass


def multiRunnerErrorCallBack(*args, **kwargs):
    obj = args[0]  # type:AutoService
    msg = args[1]
    obj.log(f"{obj.getNum()}运行失败,{msg}")


'''配置文件读写'''


class Properties:
    def __init__(self, filename, encoding='utf-8'):
        self.filename = filename
        self.fp_read = open(self.filename, 'r', encoding=encoding)
        self.data = self.fp_read.readlines()
        self.fp_read.close()
        self.properties = {}
        for i in self.data:
            if '#' in i:
                continue
            sp = i.split('=')
            key = sp[0].replace(' ', '')
            value = sp[-1].replace('\n', '').replace(' ', '')
            self.properties.update({key: value})

    def get(self, key):
        return self.properties[key]

    def set(self, key, value):
        self.fp_write = open(self.filename, 'w', encoding='utf-8')
        try:
            self.properties[key] = value
        except:
            pass
        data = ""
        for k, v in self.properties.items():
            data += k + " = " + v + "\n"

        self.fp_write.writelines(data[:-1])
        self.fp_write.close()


"""装饰器"""


def reLoadSession(CheckCall="checkSession"):
    '''
    自动读取类下的checkSession方法，也可以手动指定。
    :param CheckCall:
    :return:
    '''

    def decorator(func, *args, **kwargs):
        # print("decorator",func)
        def inner(*args, **kwargs):
            t = args[0]  # type: object
            s = t.__getattribute__("storage_session")
            if s != None and len(s) > 0:
                if type(CheckCall) == str:
                    if t.__getattribute__(CheckCall)():
                        return t
                elif type(CheckCall).__name__ == "function":
                    if CheckCall(t):
                        return t
            return func(*args, **kwargs)

        return inner

    return decorator

def default(need_decorate_function_name):
    """标注缺省时候的flow方法"""
    need_decorate_function_name.__annotations__['default']=1
    setattr(need_decorate_function_name,'default',1)
    return need_decorate_function_name

def dataUnTracker():
    """
    取消数据跟踪，这样就不会自动保存数据
    对象复用，请把此变量放到清理所在的方法上，这样修改storage_开头的变量等就不会自动保存啦
    :return:
    """

    def decorator(func, *args, **kwargs):
        def inner(*args, **kwargs):
            t = args[0]  # type: object
            __autoLoadEnv__ = t.__getattribute__("__autoLoadEnv__")
            object.__setattr__(t, "__autoLoadEnv__", False)
            val = func(*args, **kwargs)
            object.__setattr__(t, "__autoLoadEnv__", __autoLoadEnv__)
            return val

        return inner

    return decorator


'''面向对象版'''


class AutoService:
    '''
    调用前一定要记得先init调用初始手机号.
    调用self.getNum()可以得到手机号码
    有完整流程的使用flow_方法名标记函数。
    flow_login表示有完整的登录流程，如果需要重复使用token，可以加入装饰器reLoadSession("[cookie检查方法]")
    '''
    _APPLICATION_NAME = ""  # 程序名称
    _PHONENUM = ""
    _SMS_UNION_ID = 0
    APPLICATION_NAME = None
    _Properties = None
    __autoLoadEnv__ = True  # 是否自动加载变量
    __OPEN_CHROME__ = False  # 浏览器交互模式

    __disable__load__wait__ = False  # 有的时候登录需要验证码，有的时候不需要，比如在常用登录地址上，所以就有了这个检测，如果开启了，则不用等待检测,但是需要在发送验证码那个地方调用等待.

    def getOutFileName(self, title: str, suffix=""):
        return f"{DIR_OUTPUT}/{self._APPLICATION_NAME}_{self.getNum()}_{title}{suffix}"

    def getNum(self):
        """获取手机号"""
        return self._PHONENUM

    # storage_ 变量监控
    def __setattr__(self, key, value):
        callName = traceback.extract_stack()[-2][2]
        if callName == "conf_auto_load":
            return object.__setattr__(self, key, value)
        elif (hasattr(self, "__autoLoadEnv__") or "__autoLoadEnv__" in self) and not self.__autoLoadEnv__:
            pass
        elif type(key) == str and key.startswith("storage_"):
            self._Properties.set(key.replace("storage_", ""), str(value))
        object.__setattr__(self, key, value)

    # 加载配置到变量
    def conf_auto_load(self):

        filename = f"{self._APPLICATION_NAME}_{self._PHONENUM}"
        conf_name = f"{DIR_STORAGE}/{filename}.properties"
        # print("加载配置", conf_name)
        if os.path.exists(conf_name):
            self._Properties = Properties(conf_name)
            # print("加载配置", conf_name)
            for key in self._Properties.properties:
                object.__setattr__(self, f"storage_{key}", self._Properties.properties[key])
        else:
            with open(conf_name, 'w') as f:
                pass
            self._Properties = Properties(conf_name)

            # with open(conf_save_dir + "/" + filename, "r") as f:
            #     for t in f.readlines():
            #         if t.index("# ") >= 0:
            #             t = t.strip().split("# ")
            #         if t.strip() == "":
            #             continue
            #         if t.find("=")>0:
            #             iniConfig=t.split("=",1)
            #             self.__setattr__("storage_"+iniConfig[0],iniConfig[1])

    def __init__(self, appName=""):
        self.__app_startTime=int(time.time()*1000)
        self.driver = None
        if appName and len(appName) > 0:
            pass
        elif (self.APPLICATION_NAME):
            appName = self.APPLICATION_NAME
        else:
            appName = self.__class__.__name__
        self._APPLICATION_NAME = appName

    def __del__(self):
        if self.driver is not None:
            try:
                log("即将关闭浏览器驱动", model="AutoService", level="DEBUG")
                self.driver.quit()
            except:
                pass

        print(f"=======对象运行结束,耗时:{(int(time.time() * 1000)-self.__app_startTime)/1000}s==========")

    """json数据导出器"""

    def tojson(self, data: dict):
        return json.dumps(data, ensure_ascii=False)

    def log(self, *args, **kwargs):
        """
        日志方法
        :param level: ['DEBUG','WARN','ERROR']
        :return:
        """
        log(*args, model=self._APPLICATION_NAME, **kwargs)

    def init(self, phoneNum, autoLoadEnv=None, **kwargs):
        '''
        调用此方法初始化手机号信息
        :param phoneNum:
        :return:
        '''
        self._PHONENUM = phoneNum
        while not check_node_status():
            log("节点链接失败，60s后重连", model="autoService", level="WARN")
            time.sleep(60)
        if not self.__disable__load__wait__:
            while not checkUserUsePhone(self._PHONENUM):
                time.sleep(10)
        self.getlast_union()
        if autoLoadEnv is not None:
            self.__autoLoadEnv__ = autoLoadEnv

        if self.__OPEN_CHROME__:
            if (self.driver == None):
                from .chromeDriver import getDriver
                log("即将初始化浏览器驱动", model="AutoService", level="DEBUG")
                self.driver = getDriver()
                log("初始化成功", model="AutoService", level="DEBUG")

        if self.__autoLoadEnv__:
            self.conf_auto_load()
        return self

    def checkE(self, express: bool, *args):
        return errorCheck(express, *args, app_name=self._APPLICATION_NAME)

    def getlast_union(self):
        '''
        初始化短信API服务
        '''
        # sms_union_id
        try:
            data = requests.get(nodered_gateway + f'/getVerCode?num={self._PHONENUM}').json()
            self._SMS_UNION_ID = data["union"]
            return True
        except Exception as e:
            print(e)
            return False

    def checkUserUsePhone(self):
        '''
        检测手机是否正在使用，或者说是否能正常接收验证码。
        '''
        checkUserUsePhone(self.getNum())

    def sms_send(self, calls=lambda x: x, *args, **kwargs):
        '''
        可以把短信发送的代码放到这里来，也可以直接先调用super.sms_send再执行验证码发送的代码
        :param calls:
        :param args:
        :param kwargs:
        :return:
        '''
        # 如果为延迟检测的话，就需要进行判断手机是否处于解锁状态咯
        if self.__disable__load__wait__:
            self.checkUserUsePhone()
        return sms_send(calls, *args, phone=self._PHONENUM, application=self._APPLICATION_NAME, **kwargs)

    def getVerCode(self, must=False):
        data, self._SMS_UNION_ID = getSmsVerCode(self._PHONENUM, call_is_obj=True, union_id=self._SMS_UNION_ID)
        if must:
            self.checkE(len(data) > 0, "验证码获取失败!")
        # print("验证码为:"+data)
        return data

    # 工具函数列表
    def post(self, url, data):
        log("post", url, data, model="NetWorkTransfer", level="DEBUG")
        if type(data) == dict:
            data = json.dumps(data)
        data = requests.post(url, data=data, headers={"Content-Type": "application/json",
                                                      "user-agent": "Mozilla/5.0 (Linux; Android 12; M2104K10AC Build/SP1A.210812.016; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/96.0.4664.104 Mobile Safari/537.36 uni-app Html5Plus/1.0 (Immersed/34.909092)"}).json()
        log("post", "recv", data, model="NetWorkTransfer", level="DEBUG")
        return data


def getApplication() -> AutoService:
    """
    获取当前文件中定义的自动化类
    :return:
    """
    caller_frame = sys._getframe(1)
    caller_obj = caller_frame.f_locals
    for t in caller_obj:
        if (type(caller_obj.get(t)) == type):
            if (t == 'AutoService'): continue
            # print(t)
            if issubclass(caller_obj.get(t), AutoService):
                return caller_obj.get(t)()
    log("无法找到当前文件中的自动化服务,如果有请记得继承AutoService类", model="autoService", level="ERROR")
    return None


App = getApplication # getApplication的别名




def multiRunner(class_type: type, flow: str, error_do=multiRunnerErrorCallBack):

    """

    批量运行工具，批量运行器
    读取json文件，将对应键映射传递给init方法，然后执行flow所对应的方法，如果出错，则会运行error_do
    :param class_type: 类名
    :param flow: 流程名称
    :param error_do: 错误回调
    :return:
    """

    if not os.path.exists(f"{DIR_CONF}/{class_type.__name__}.json"):
        log(f"未读取到配置文件,路径为:{DIR_CONF}/{class_type.__name__}.json", model="multiRunner")
        return
    result = None
    try:
        with open(f"{DIR_CONF}/{class_type.__name__}.json") as f:
            result = json.load(f)
    except Exception as e:
        return
    if result == None:
        log(f"未读取到配置文件内容,路径为:{DIR_CONF}/{class_type.__name__}.json", model="multiRunner")
        return
    if type(result) == dict:
        result = [result]
    if type(result) != list:
        log("请提供正确的json数据!", model="multiRunner")
        return

    needReCrate = True
    obj = None
    for c in result:
        try:
            if needReCrate:
                obj = class_type.__new__(class_type)
                obj.__init__()
            getattr(obj, "init")(**c)
            getattr(obj, flow)()
            if hasattr(obj, "destroy"):
                getattr(obj, "destroy")()
                needReCrate = False

        except Exception as e:
            if hasattr(obj, "errorCallBack"):
                getattr(obj, "errorCallBack")(e)
            else:
                error_do(obj, e)
